package com.google.jessewilson.codesearch.util;

import ca.odell.glazedlists.EventList;
import ca.odell.glazedlists.SortedList;
import ca.odell.glazedlists.TransformedList;
import ca.odell.glazedlists.event.ListEvent;
import ca.odell.glazedlists.event.ListEventListener;
import ca.odell.glazedlists.impl.adt.Barcode;
import ca.odell.glazedlists.impl.adt.BarcodeIterator;

import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;

/**
 * Contains elements that intersect another EventList.
 *
 * @author jessewilson
 */
public class IntersectionList<E> extends TransformedList<E, E> {

  private SortedList<E> sortedSource;
  private Barcode barcode = new Barcode();

  private List<Intersector> intersectors = new ArrayList<Intersector>();

  public IntersectionList(EventList<E> source, Comparator<E> comparator) {
    super(new SortedList<E>(source, comparator));
    this.sortedSource = (SortedList<E>)super.source;

    // everything that's included is black
    this.barcode.addBlack(0, source.size());

    // listen for changes to the source list
    this.sortedSource.addListEventListener(this);
  }


  public void dispose() {
    for(Intersector intersector : intersectors) {
      intersector.dispose();
    }
    sortedSource.removeListEventListener(this);
    sortedSource.dispose();

    sortedSource = null;
    barcode = null;
    intersectors = null;
  }

  protected int getSourceIndex(int i) {
    return barcode.getIndex(i, Barcode.BLACK);
  }

  public int size() {
    return barcode.blackSize();
  }

  public void addIntersection(EventList<E> list) {
    if(list.getReadWriteLock() != getReadWriteLock()) {
      throw new IllegalArgumentException("Locks must be the same!");
    }
    intersectors.add(new Intersector(list));
    rebuild();
  }

  /**
   * Mostly copied from FilterList, this reevaluates all elements for intersection.
   */
  private void rebuild() {
    updates.beginEvent();
    // for all source items, see what the change is
    for(BarcodeIterator i = barcode.iterator(); i.hasNext(); ) {
        i.next();

        // determine if this value was already filtered out or not
        int filteredIndex = i.getBlackIndex();
        boolean wasIncluded = filteredIndex != -1;
        // whether we should add this item
        E value = source.get(i.getIndex());
        boolean include = allIntersectorsContain(value);

        // this element is being removed as a result of the change
        if(wasIncluded && !include) {
            i.setWhite();
            updates.elementDeleted(filteredIndex, value);

        // this element is being added as a result of the change
        } else if(!wasIncluded && include) {
            updates.elementInserted(i.setBlack(), value);
        }
    }
    updates.commitEvent();
  }

  /**
   * Returns true if all the intersectors contain the specified value.
   */
  private boolean allIntersectorsContain(E value) {
    for (Intersector s : intersectors) {
      if(!s.contains(value)) {
        return false;
      }
    }
    return true;
  }

  private void reevaluate(E changedInIntersector) {
    int firstIndex = sortedSource.sortIndex(changedInIntersector);
    int lastIndex = sortedSource.lastSortIndex(changedInIntersector);

    for(int i = firstIndex; i <= lastIndex && i < sortedSource.size(); i++) {
      E value = sortedSource.get(i);
      boolean isIncluded = barcode.get(i) == Barcode.BLACK;
      boolean shouldBeIncluded = allIntersectorsContain(value);

      if(shouldBeIncluded && !isIncluded) {
        barcode.setBlack(i, 1);
        int filteredIndex = barcode.getBlackIndex(i);
        updates.elementInserted(filteredIndex, value);
      } else if(!shouldBeIncluded && isIncluded) {
        int filteredIndex = barcode.getBlackIndex(i);
        barcode.setWhite(i, 1);
        updates.elementDeleted(filteredIndex, value);
      }
    }
  }


  public void listChanged(ListEvent<E> listChanges) {
    updates.beginEvent(true);

    while(listChanges.next()) {
      // get the current change info
      int changedIndex = listChanges.getIndex();
      int changeType = listChanges.getType();

      // for inserts and updates, insert the new object
      if(changeType == ListEvent.INSERT || changeType == ListEvent.UPDATE) {
        E added = sortedSource.get(changedIndex);
        boolean intersect = allIntersectorsContain(added);
        if(intersect) {
          barcode.addBlack(changedIndex, 1);
          int blackIndex = barcode.getBlackIndex(changedIndex);
          updates.elementInserted(blackIndex, added);
        } else {
          barcode.addWhite(changedIndex, 1);
        }
      }

      // for updates and deletes, remove the old object
      if(changeType == ListEvent.UPDATE || changeType == ListEvent.DELETE) {
        int filteredIndex = barcode.getBlackIndex(changedIndex);
        barcode.remove(changedIndex, 1);
        if(filteredIndex != -1) {
          updates.elementDeleted(filteredIndex, listChanges.getOldValue());
        }
      }
    }
    updates.commitEvent();
  }

  private class Intersector implements ListEventListener<E> {
    private final List<E> cache;
    private final SortedList<E> sorted;

    public Intersector(EventList<E> list) {
      this.sorted = new SortedList<E>(list, sortedSource.getComparator());
      this.cache = new ArrayList<E>(sorted);

      this.sorted.addListEventListener(this);
    }

    public void listChanged(ListEvent listChanges) {
      updates.beginEvent();
      while(listChanges.next()) {
        // get the current change info
        int changedIndex = listChanges.getIndex();
        int changeType = listChanges.getType();

        if(changeType == ListEvent.INSERT) {
          E added = sorted.get(changedIndex);
          cache.add(changedIndex, added);
          reevaluate(added);
        } else if(changeType == ListEvent.UPDATE) {
          E updated = sorted.get(changedIndex);
          cache.set(changedIndex, updated);
          reevaluate(updated);
        } else if(changeType == ListEvent.DELETE) {
          E deleted = cache.remove(changedIndex);
          reevaluate(deleted);
        }
      }
      updates.commitEvent();
    }

    public void dispose() {
      this.sorted.removeListEventListener(this);
      this.sorted.dispose();
    }

    public boolean contains(E value) {
      // search in the cache because the source might be changing
      int index = Collections.binarySearch(cache, value, sortedSource.getComparator());
      return index >= 0;
    }
  }
}
